import { App, Provide, Inject } from '@midwayjs/decorator'
import { CacheManager } from '@midwayjs/cache'

import { Application, Context } from 'egg'
import { gql } from 'graphql-request'

import { AuthService } from './auth'
import { getRealIp, hasKeyIn } from '../utils/helper'

const bcrypt = require('bcrypt')
const _ = require('lodash')

enum GetQueryKeys {
  USERNAME = 'username',
  PHONE = 'phone',
  EMAIL = 'email',
  ID = 'id',
  OPENID = 'openid'
}

const READONLY = ['username', 'email', 'phone']
const TOKEN_SENSTIVE = ['password', 'active', 'role.id', 'is_system_user']

export const UserFragment = `
  id
  username
  password
  phone
  email
  active
  last_ip
  last_login_at
  nickname
  realname
  avatar
  user_type
  openid
  remark
  deleted_at
  create_at
  update_at
  role: ie_user_role {
    id
    role
    rolename
  }
  user_meta
`

@Provide()
export class UserService {

  @App()
  app: Application

  @Inject()
  ctx: Context

  @Inject()
  cache: CacheManager

  @Inject()
  authService: AuthService

  async get(query: {
    [key in GetQueryKeys]?: string
  }) {
    const where = {}

    Object.keys(query).forEach(key => {
      where[key] = { _eq: query[key] }
    })

    try {
      const { ie_user: [user] } = await this.app.gqlRequest(gql`
        query ($where: ie_user_bool_exp!) {
          ie_user (where: $where) {
            ${UserFragment}
          }
        }
      `, { where })

      return user
    } catch (err) {}
  }

  async add(user) {
    const {
      password,
      ...userProps
    } = user

    // weapp password
    if (password) {
      userProps.password = await bcrypt.hash(password, 10)
    }

    // insert empty user_meta
    userProps.user_meta = {
      data: {}
    }

    try {
      const {
        insert_ie_user: {
          returning: [addedUser]
        }
      } = await this.app.gqlRequest(gql`
        mutation ($user: ie_user_insert_input!) {
          insert_ie_user (objects: [$user]) {
            affected_rows
            returning {
              ${UserFragment}
            }
          }
        }
      `, {
        user: userProps
      })
  
      return addedUser
    } catch (err) {
      this.app.logger.error(err)
    }
  }

  async update(updated) {
    const safeUpdated = _.omit(updated, READONLY)
    const { id, password, ...user } = safeUpdated

    if (password) {
      user.password = await bcrypt.hash(password, 10)
    }

    // marked token changed
    if (hasKeyIn(safeUpdated, TOKEN_SENSTIVE)) {
      await this.cache.set(id, true, { ttl: 7 * 24 * 3600 })
    }

    if (id) {
      const {
        update_ie_user: {
          returning: [updatedUser]
        }
      } = await this.app.gqlRequest(gql`
        mutation ($id: uuid, $user: ie_user_set_input!) {
          update_ie_user (
            where: { id: { _eq: $id } }
            _set: $user
          ) {
            returning {
              ${UserFragment}
            }
          }
        }
      `, {
        id,
        user
      })

      return updatedUser
    }
  }

  async validate(password, hashed) {
    return await bcrypt.compare(password, hashed)
  }

  async afterLogin(user, remember_me) {
    const payload = _.pick(user, [
      'id',
      'username',
      'role',
      'user_type',
      'active'
    ])

    payload.is_system_user = false

    if (payload.role?.role) {
      payload.role = payload.role.role
    }

    // add token
    user.token = await this.authService.generateToken(payload)

    const tokenExpired = await this.cache.get(payload.id)

    if (tokenExpired) {
      if (remember_me) {
        user.refresh_token = await this.authService.generateToken(payload, true)
      } else {
        await this.authService.removeRefreshToken(payload.id)
      }
    } else if (remember_me) {
      user.refresh_token =
        await this.authService.getRefreshToken(payload.id) ||
        await this.authService.generateToken(payload, true)
    }

    // add last login ip + time
    const updateProps = {
      last_ip: getRealIp(this.ctx),
      last_login_at: 'now()'
    }

    const { last_login_at } = await this.update({
      id: user.id,
      ...updateProps
    })

    return {
      ..._.omit(user, ['openid']),
      ...updateProps,
      last_login_at
    }
  }
}
